/*******************************************************************************
 * This file is part of Crunch Network.
 *
 * Crunch Network is free software: you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * Crunch Network is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along with
 * Crunch Network.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

package com.crunch.network;

import java.util.*;

/**
 * TODO
 * this is used for "host side" only
 */
public class GhostManager {
	/**
	 * Constructs a ghost manager allowing for {@link com.crunch.network.NetworkSettings#maxLocalGhosts} ghosts to exist
	 * at a time.
	 *
	 * @param   writer a reference to the {@link NetworkWriter} instance.
	 * @param   settings the network settings.
	 */
	GhostManager(NetworkWriter writer, NetworkSettings settings) {
		if (settings.maxLocalGhosts < 0) {
			throw new IllegalArgumentException("Invalid max ghosts");
		}

		this.writer = writer;
		this.settings = settings;

		ghostIdBits = 1;
		int ghostRange = 2;
		while (ghostRange < settings.maxLocalGhosts) {
			++ghostIdBits;
			ghostRange *= 2;
		}

		freeIdQueue = new ArrayDeque<Integer>();
		nextFreeId = 0;

		ghosts = new HashMap<Integer, Ghost>();
		sortedGhosts = new ArrayList<Integer>();
	}

	/**
	 * Returns the maximum number of ghosts allowed to exist at one time.
	 *
	 * @return  the maximum number of ghosts allowed to exist at one time.
	 */
	public int getMaxGhosts() {
		return settings.maxLocalGhosts;
	}

	/**
	 * Returns the number of bits used in ghost IDs.
	 *
	 * @return  the number of bits used in ghost IDs.
	 */
	public int getGhostIdBits() {
		return ghostIdBits;
	}

	/**
	 * Creates a ghost associated with a previously unused ID.
	 *
	 * @param   stateTable the state table associated with the ghost.
	 * @param   priority the initial priority of the ghost.
	 * @return  the ID of the new ghost.
	 * @throws  IllegalStateException if the maximum number of ghosts already exist.
	 * @throws  IllegalArgumentException if the priority is less than 0.
	 */
	public int createGhost(StateTable stateTable, float priority) {
		if (getGhostCount() >= settings.maxLocalGhosts) {
			throw new IllegalStateException("Maximum ghost limit exceeded");
		}
		if (priority < 0.0f) {
			throw new IllegalArgumentException("Negative priority " + priority);
		}

		// if the max ghosts is less than the ID reuse cycle size, compare against that instead
		int idReuseCycleSize = Math.min(ID_REUSE_CYCLE_SIZE, settings.maxLocalGhosts);
		int id;
		if (freeIdQueue.size() < idReuseCycleSize) {
			id = nextFreeId;
			++nextFreeId;
		} else {
			id = freeIdQueue.remove();
		}

		Ghost ghost = ghosts.get(id);
		if (ghost == null) {
			ghost = new Ghost();
			ghost.dependency = new MessageDependency();
			ghosts.put(id, ghost);
		}
		ghost.stateTable = stateTable;
		ghost.inUse = true;
		ghost.priority = priority;
		ghost.lastUpdateTimeMs = settings.callbacks.getCurrentTimeMs();

		// write ghost creation message
		writer.writeCreateGhost(stateTable, id, new MessageDependency[] { ghost.dependency }, ghost.dependency);

		return id;
	}

	/**
	 * Destroys the ghost associated with the given ID and allows the ID to be reused. The state table should not be
	 * reused until a call to {@link MessageRegistry.StateTableFactory#destroy(StateTable)} is called with the state
	 * table, which will happen automatically after this method is called (but possibly not immediately).
	 *
	 * @param   id the ID of the ghost to destroy.
	 * @throws  IllegalArgumentException if the ID provided is not currently associated with a ghost.
	 */
	public void destroyGhost(int id) {
		Ghost ghost = ghosts.get(id);
		if (ghost == null) {
			throw new IllegalArgumentException("Ghost " + id + " is not currently in use");
		}

		// write ghost destruction message
		writer.writeDestroyGhost(ghost.stateTable, id, new MessageDependency[] { ghost.dependency }, ghost.dependency);

		ghost.inUse = false;
		freeIdQueue.add(id);
	}

	/**
	 * Sets the priority of the ghost with the given ID. The ID must be in use and the priority must be greater than or
	 * equal to 0.
	 *
	 * @param   id the ID of the ghost to set the priority of.
	 * @param   priority the priority to assign.
	 * @throws  IllegalArgumentException if the ID provided is not currently associated with a ghost, or if the priority
	 *          is less than 0.
	 */
	public void setGhostPriority(int id, float priority) {
		Ghost ghost = ghosts.get(id);
		if (ghost == null) {
			throw new IllegalArgumentException("Ghost " + id + " is not currently in use");
		}
		if (priority < 0.0f) {
			throw new IllegalArgumentException("Negative priority " + priority);
		}

		ghost.priority = priority;
	}

	/**
	 * Writes updates for each active ghost. A single packet may not have space for all active ghost data, so ghost
	 * updates are ordered and cycled through by priority and time since the last update was sent.
	 */
	boolean writeGhostUpdates() {
		if (writer.hasError()) {
			return false;
		}

		// sort the ghosts by (-time_since_last_update) * priority
		// this causes ghosts with a high priority and longer time since last update to occur first
		sortedGhosts.clear();
		for (Map.Entry<Integer, Ghost> e : ghosts.entrySet()) {
			if (e.getValue().inUse && e.getValue().stateTable.requiresStateUpdate()) {
				sortedGhosts.add(e.getKey());
			}
		}

		final long currentTimeMs = settings.callbacks.getCurrentTimeMs();
		Collections.sort(sortedGhosts, new Comparator<Integer>() {
			@Override
			public int compare(Integer a, Integer b) {
				Ghost ghostA = ghosts.get(a);
				Ghost ghostB = ghosts.get(b);
				long negTimeSinceLastUpdateA = ghostA.lastUpdateTimeMs - currentTimeMs;
				long negTimeSinceLastUpdateB = ghostB.lastUpdateTimeMs - currentTimeMs;
				float sortValA = (float) negTimeSinceLastUpdateA * ghostA.priority;
				float sortValB = (float) negTimeSinceLastUpdateB * ghostB.priority;
				if (sortValA < sortValB) {
					return -1;
				} else if (sortValA > sortValB) {
					return 1;
				} else {
					return 0;
				}
			}
		});

		// in order, write as many update messages as we can fit into the packet and update last updated times
		MessageDependency[] depArray = new MessageDependency[1];
		for (Integer id : sortedGhosts) {
			Ghost g = ghosts.get(id);
			depArray[0] = g.dependency;
			if (!writer.writeUpdateGhost(g.stateTable, id, depArray)) {
				// ran out of space, so stop updating
				break;
			}

			g.lastUpdateTimeMs = currentTimeMs;
		}

		return writer.hasError();
	}

	/**
	 * Returns the number of currently active ghosts.
	 *
	 * @return  the number of currently active ghosts.
	 */
	public int getGhostCount() {
		return ghosts.size() - freeIdQueue.size();
	}

	/**
	 * Returns whether the given ID is currently associated with a ghost.
	 *
	 * @param   id the ID to check.
	 * @return  whether the ID is currently associated with a ghost.
	 */
	public boolean isGhost(int id) {
		Ghost ghost = ghosts.get(id);
		return (ghost != null) && ghost.inUse;
	}

	/**
	 * Returns the {@link MessageDependency} associated with the given ID, which may be empty. The {@link
	 * MessageDependency} associated with a particular ID is used to ensure that events within a single ghost are
	 * properly ordered, and that events are also properly ordered in cases where ghost IDs are reused shortly after
	 * being freed. The {@link MessageDependency} instances associated with IDs are made publicly available so that
	 * additional, user-defined messages (other than creation, update, and destruction) can be sent and ordered properly
	 * for a the ghost currently assigned to the ID.
	 *
	 * @param   id the ID for which to return the associated {@link MessageDependency}.
	 * @return  the {@link MessageDependency} associated with the ID. If the ID has never been used, {@code null} is
	 *          returned.
	 */
	public MessageDependency getMessageDependency(int id) {
		Ghost ghost = ghosts.get(id);
		if (ghost == null) {
			return null;
		} else {
			return ghost.dependency;
		}
	}

	private NetworkSettings settings;           // the network settings
	private NetworkWriter writer;               // the network writer
	private int ghostIdBits;                    // number of bits per ghost ID

	// minimum number of IDs that must be cycled through before reusing IDs
	private static final int ID_REUSE_CYCLE_SIZE = 20;
	// queue of IDs that have been freed
	private Queue<Integer> freeIdQueue;
	// next ID that has never been used
	private int nextFreeId;

	// data associated with a ghost
	private static class Ghost {
		// whether this ghost is in use
		public boolean inUse;

		// the state table associated with this ghost
		public StateTable stateTable;

		// time the last update was sent out
		public long lastUpdateTimeMs;

		// the priority of the ghost, used to determine update frequency compared to other ghosts
		public float priority;

		// the dependency associated with this ID
		// used to make sure there are no conflicts when reusing IDs
		public MessageDependency dependency;
	}
	// map of ghost data
	private HashMap<Integer, Ghost> ghosts;

	// used to sort ghosts for priority-based updates
	private ArrayList<Integer> sortedGhosts;
}
